/**
 * 
 */
package ch.gje.playios.server.handler;

import ch.gje.playios.communication.ComputerClient;
import ch.gje.playios.communication.IOSClient;
import ch.gje.playios.communication.WebSocketClient;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

import ch.gje.playios.server.HTTPConnection;
import ch.gje.playios.server.exception.OldWebSocketStandardException;

/**
 * Handles WebSocket request
 * 
 * @author jannis <jannis@gje.ch
 */
public class WebSocketHandler extends RequestHandler {

    /**
     * Protocol name for computer clients
     */
    public static String SUBPROTOCOL_COMPUTER = "computer.play-ios";
    /**
     * Protocol name for iOS clients
     */
    public static String SUBPROTOCOL_IOS = "ios.play-ios";
    /**
     * The request headers
     */
    private Map<String, String> headers;

    /**
     * Constructor
     *
     * @param connection
     *            the HTTP connection
     */
    public WebSocketHandler(HTTPConnection connection) {
        super(connection);
        headers = connection.getHeaders();
        handle();
    }

    /*
     * (non-Javadoc)
     *
     * @see server.QueryHandler#handleQuery(java.lang.String, java.lang.String)
     *
     * ByteBuffer handling inspired by TooTallNate's Java-WebSocket on GitHub,
     * MIT-Licence, Copyright (c) 2010 Nathan Rajlich
     */
    @Override
    public void handleQuery() throws IOException, NoSuchAlgorithmException {
        String subprotocol = doHandshake();
        if (!connection.isClosed()) { // Handshake successful
            WebSocketClient client;
            if (subprotocol.equals(SUBPROTOCOL_COMPUTER)) {
                System.out.println("   New WebSocket client (Computer) connected.");
                client = new ComputerClient(connection);
            } else if (subprotocol.equals(SUBPROTOCOL_IOS)) {
                System.out.println("   New WebSocket client (iOS) connected.");
                client = new IOSClient(connection);
            } else {
                // unknown protocol: quit.
                return;
            }
            int read = 0;
            byte[] buffer;
            ByteBuffer currentFrame = ByteBuffer.allocate(0);
            boolean outsideFrame = true;
            boolean closingStarted = false;
            boolean closingHandshakeSent = false;
            while (read != -1 && !closingHandshakeSent) {
                buffer = new byte[100];
                read = connection.read(buffer);
                if (read == -1) {
                    // Handle temporary network delay
                    try {
                        Thread.sleep(400);
                    } catch (InterruptedException e) {
                        // do nothing
                    }
                    read = connection.read(buffer);
                }
                for (int i = 0; i < read && !closingHandshakeSent; i++) {
                    if (buffer[i] == (byte) 0x00) {
                        if (closingStarted) {
                            byte[] closingHandshake = {(byte) 0xFF, (byte) 0x00};
                            connection.send(closingHandshake);
                            closingHandshakeSent = true;
                        } else {
                            outsideFrame = false;
                            currentFrame = ByteBuffer.allocate(0);
                        }
                    } else if (buffer[i] == (byte) 0xFF) {
                        if (outsideFrame) {
                            closingStarted = true;
                            outsideFrame = false;
                        } else {
                            String message = new String(currentFrame.array(),
                                    Charset.forName("UTF-8"));
                            System.out.println(message);
                            client.onReceive(message);
                            outsideFrame = true;
                        }
                    } else {
                        ByteBuffer frame = ByteBuffer.allocate(currentFrame.capacity() + 1);
                        currentFrame.rewind();
                        frame.put(currentFrame);
                        frame.put(buffer[i]);
                        currentFrame = frame;
                    }
                }
            }
            connection.close();
            client.onClose();
            System.out.println("   Socket closed.");
        }
    }

    /**
     * Does the WebSocket handshake
     *
     * @return The subprotocol for this connection
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    private String doHandshake() throws IOException, NoSuchAlgorithmException {
        String string1 = headers.get("Sec-WebSocket-Key1");
        String string2 = headers.get("Sec-WebSocket-Key2");
        byte[] request = connection.getRequest();
        String subprotocol = headers.get("Sec-WebSocket-Protocol");
        int start = -1;
        int index = 4;
        try {
            while (start == -1) {
                if (index < request.length - 8) {
                    if (request[index - 4] == '\r'
                            && request[index - 3] == '\n'
                            && request[index - 2] == '\r'
                            && request[index - 1] == '\n') {
                        start = index;
                    }
                } else {
                    throw new OldWebSocketStandardException();
                }
                index++;
            }
            byte[] key3 = Arrays.copyOfRange(request, start, start + 8);

            if (string1 == null || !string1.contains(" ") || string2 == null
                    || !string2.contains(" ")) {
                new ForbiddenHandler(connection,
                        "No valid WebSocket handshake.");
            } else if (subprotocol == null || !subprotocol.equals(SUBPROTOCOL_COMPUTER)
                    && !subprotocol.equals(SUBPROTOCOL_IOS)) {
                new ForbiddenHandler(connection,
                        "Unknown WebSocket subprotocol.");
            } else {
                setResponseStatus(101, "WebSocket Protocol Handshake");
                setHeader("Upgrade", "WebSocket");
                setHeader("Connection", "close");
                deleteHeader("Content-Type");
                setHeader("Sec-WebSocket-Protocol", subprotocol);
                setHeader("Sec-WebSocket-Origin", headers.get("Origin"));
                setHeader(
                        "Sec-WebSocket-Location",
                        "ws://" + headers.get("Host")
                        + headers.get(HTTPConnection.REQUEST_RESOURCE));
                sendHeaders();

                String number1 = "";
                int spaces1 = 0;
                for (char c : string1.toCharArray()) {
                    if (Character.isDigit(c)) {
                        number1 += c;
                    } else if (c == ' ') {
                        spaces1++;
                    }
                }
                long key1 = Long.valueOf(number1) / spaces1;

                String number2 = "";
                int spaces2 = 0;
                for (char c : string2.toCharArray()) {
                    if (Character.isDigit(c)) {
                        number2 += c;
                    } else if (c == ' ') {
                        spaces2++;
                    }
                }
                long key2 = Long.valueOf(number2) / spaces2;

                /*
                 * Little bit of byte shifting — inspired by TooTallNate's
                 * Java-WebSocket on GitHub, MIT-Licence, Copyright (c) 2010
                 * Nathan Rajlich
                 */
                byte[] bytes = new byte[16];
                bytes[0] = (byte) (key1 >> 24);
                bytes[1] = (byte) ((key1 << 8) >> 24);
                bytes[2] = (byte) ((key1 << 16) >> 24);
                bytes[3] = (byte) ((key1 << 24) >> 24);
                bytes[4] = (byte) (key2 >> 24);
                bytes[5] = (byte) ((key2 << 8) >> 24);
                bytes[6] = (byte) ((key2 << 16) >> 24);
                bytes[7] = (byte) ((key2 << 24) >> 24);
                System.arraycopy(key3, 0, bytes, 8, 8);

                MessageDigest md5 = MessageDigest.getInstance("MD5");
                byte[] hash = md5.digest(bytes);

                connection.send(hash);
            }
        } catch (OldWebSocketStandardException e) {
            new ForbiddenHandler(connection, "Old WebSocket Standard");
        } finally {
            return subprotocol;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see server.RequestHandler#handleServerError()
     */
    @Override
    protected void handleServerError() {
        // TODO
    }
}
